package com.zstk.frame.config.interceptor;


import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * <p>
 * mybatis/mybatis-plus模糊查询语句特殊字符转义拦截器
 * </p>
 *
 * @author ZColin
 * @since 2020/12/11 11:09
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                                                                         Object.class,
                                                                         RowBounds.class,
                                                                         ResultHandler.class})})
@Component
@Slf4j
@ConditionalOnClass(value = Interceptor.class)
public class MybatisLikeSqlInterceptor implements Interceptor {

    @Value("${spring.datasource.dbtype}")
    private String dbType;

    /**
     * SQL语句like
     */
    private final static String SQL_LIKE = " like ";

    /**
     * SQL语句like
     */
    private final static String SQL_LIKE_UPPER_CASE = " LIKE ";

    /**
     * SQL语句空格
     */
    private final static String SQL_BLANK = " ";

    /**
     * SQL语句占位符
     */
    private final static String SQL_PLACEHOLDER = "?";

    /**
     * SQL语句左括号
     */
    private final static String SQL_BRACKET = "(";

    /**
     * 所有的转义器
     */
    private static Map<Class, AbstractLikeSqlConverter> converterMap = new HashMap<>(4);

    static {
        converterMap.put(Map.class, new MapLikeSqlConverter());
        converterMap.put(Object.class, new ObjectLikeSqlConverter());
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //先暂时 只处理mysql oracle   不支持多数据源
        if(MybatisSqlConstant.MYSQL.equals(dbType) || MybatisSqlConstant.ORACLE.equals(dbType)){
            Object[] args = invocation.getArgs();
            MappedStatement statement = (MappedStatement) args[0];
            Object parameterObject = args[1];
            BoundSql boundSql = statement.getBoundSql(parameterObject);
            String sql = boundSql.getSql();
            this.transferLikeSql(sql, parameterObject, boundSql);
            if (dbType.equals(MybatisSqlConstant.MYSQL)) {
                //mysql
                sql = sql.replaceAll(SQL_LIKE, " regexp ").replaceAll(SQL_LIKE_UPPER_CASE, " regexp ");
            } else {
                // dm 或 oracle
                sql = getNewSql(sql);
            }
            resetSql2Invocation(invocation, sql);
        }
        return invocation.proceed();
    }

    private String getNewSql(String sql) {
        //是否包含like
        sql = replaceLike(sql, SQL_LIKE);

        sql = replaceLike(sql, SQL_LIKE_UPPER_CASE);

        return sql;
    }

    private String replaceLike(String sql, String upperLowerLike) {

        while (sql.contains(upperLowerLike + SQL_PLACEHOLDER)) {

            String temp = sql.substring(0, sql.indexOf(upperLowerLike + SQL_PLACEHOLDER));
            String surplus = temp.substring(temp.lastIndexOf(SQL_BLANK) + 1);
            String fieldName = surplus.replace(SQL_BRACKET, "");
            String replaceStr = fieldName + upperLowerLike + SQL_PLACEHOLDER;
            sql = sql.replace(replaceStr, "regexp_like(" + fieldName + ",?)");
        }
        return sql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties arg0) {

    }

    /**
     * 修改包含like的SQL语句
     *
     * @param sql             SQL语句
     * @param parameterObject 参数对象
     * @param boundSql        绑定SQL对象
     */
    private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
        if (!isEscape(sql)) {
            return;
        }
        sql = sql.replaceAll(" {2}", " ");
        // 获取关键字的个数（去重）
        Set<String> fields = this.getKeyFields(sql, boundSql);
        if (CollUtil.isEmpty(fields)) {
            return;
        }
        // 此处可以增强,不止是支持Map对象,Map对象仅用于传入的条件为Map或者使用@Param传入的对象被Mybatis转为的Map
        AbstractLikeSqlConverter converter;
        // 对关键字进行特殊字符“清洗”，如果有特殊字符的，在特殊字符前添加转义字符（\）
        if (parameterObject instanceof Map) {
            converter = converterMap.get(Map.class);
        } else {
            converter = converterMap.get(Object.class);
        }
        converter.convert(sql, fields, parameterObject);
    }

    /**
     * 是否需要转义
     *
     * @param sql SQL语句
     *
     * @return true/false
     */
    private boolean isEscape(String sql) {
        return this.hasLike(sql) && this.hasPlaceholder(sql);
    }

    /**
     * 判断SQL语句中是否含有like关键字
     *
     * @param str SQL语句
     *
     * @return true/false
     */
    private boolean hasLike(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_LIKE);
    }

    /**
     * 判断SQL语句中是否包含SQL占位符
     *
     * @param str SQL语句
     *
     * @return true/false
     */
    private boolean hasPlaceholder(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_PLACEHOLDER);
    }

    /**
     * 获取需要替换的所有字段集合
     *
     * @param sql      完整SQL语句
     * @param boundSql 绑定的SQL对象
     *
     * @return 字段集合列表
     */
    private Set<String> getKeyFields(String sql, BoundSql boundSql) {
        String[] params = sql.split(MybatisSqlConstant.SQL_PLACEHOLDER_REGEX);
        Set<String> fields = new HashSet<>();
        for (int i = 0; i < params.length; i++) {
            if (this.hasLike(params[i])) {
                String field = boundSql.getParameterMappings().get(i).getProperty();
                fields.add(field);
            }
        }
        return fields;
    }


    /**
     * 包装sql后，重置到invocation中
     */
    private void resetSql2Invocation(Invocation invocation, String sql) throws SQLException {
        final Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        Object parameterObject = args[1];
        BoundSql boundSql = statement.getBoundSql(parameterObject);
        MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSql));
        MetaObject msObject = MetaObject.forObject(newStatement,
                                                   new DefaultObjectFactory(),
                                                   new DefaultObjectWrapperFactory(),
                                                   new DefaultReflectorFactory());
        msObject.setValue("sqlSource.boundSql.sql", sql);
        args[0] = newStatement;
    }

    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(),
                                                                      ms.getId(),
                                                                      newSqlSource,
                                                                      ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }


    class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}
